package org.zjvis.datascience.web.controller;

import cn.hutool.core.util.ObjectUtil;
import cn.weiguangfu.swagger2.plus.annotation.ApiGroup;
import cn.weiguangfu.swagger2.plus.annotation.ApiPlus;
import cn.weiguangfu.swagger2.plus.enums.ApiExecutionEnum;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import okhttp3.Response;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.zjvis.datascience.common.annotation.DashboardAuth;
import org.zjvis.datascience.common.annotation.PipelineAuth;
import org.zjvis.datascience.common.annotation.ProjectAuth;
import org.zjvis.datascience.common.annotation.ProjectRoleAuth;
import org.zjvis.datascience.common.constant.Constant;
import org.zjvis.datascience.common.dto.DashboardDTO;
import org.zjvis.datascience.common.enums.ProjectAuthEnum;
import org.zjvis.datascience.common.enums.ProjectRoleAuthEnum;
import org.zjvis.datascience.common.exception.BaseErrorCode;
import org.zjvis.datascience.common.exception.DataScienceException;
import org.zjvis.datascience.common.model.ApiResult;
import org.zjvis.datascience.common.model.ApiResultCode;
import org.zjvis.datascience.common.util.*;
import org.zjvis.datascience.common.vo.DashboardVO;
import org.zjvis.datascience.common.widget.constant.WordTemplateConstant;
import org.zjvis.datascience.common.widget.dto.WidgetDTO;
import org.zjvis.datascience.common.widget.enums.DashboardTypeEnum;
import org.zjvis.datascience.common.widget.enums.WidgetTypeEnum;
import org.zjvis.datascience.common.widget.vo.DashboardTriggerResultVO;
import org.zjvis.datascience.service.DashboardService;
import org.zjvis.datascience.service.ProjectService;
import org.zjvis.datascience.service.UserProjectService;
import org.zjvis.datascience.service.WidgetService;
import org.zjvis.datascience.service.kg.KnowledgeGraphService;
import org.zjvis.datascience.service.snapshot.DashboardSnapshotManagerService;
import org.zjvis.datascience.service.snapshot.SnapshotManager;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.*;
import java.net.URLEncoder;
import java.util.Base64;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import static org.zjvis.datascience.common.widget.constant.WidgetJsonConstant.GRID_ITEMS;
import static org.zjvis.datascience.common.widget.constant.WidgetJsonConstant.WIDGET_ID;

/**
 * @description 仪表盘管理接口 Controller
 * @date 2021-12-01
 */
@Api(tags = "仪表盘管理")
@RestController
@RequestMapping("/dashboard")
@ApiPlus(value = true)
public class DashboardController {

    private final static Logger logger = LoggerFactory.getLogger(DashboardController.class);

    @Autowired
    private DashboardService dashboardService;

    @Lazy
    @Autowired
    private WidgetService widgetService;

    @Autowired
    private UserProjectService userProjectService;

    @Autowired
    private ProjectService projectService;

    @Autowired
    private KnowledgeGraphService knowledgeGraphService;

    @Autowired
    private DashboardSnapshotManagerService dashboardManagerService;
    @Autowired
    RedisUtil redisUtil;

    private final static String HTTP_REDIS_PREFIX = "urlForImportHttpData_";

    @PostMapping("/redis/{url}")
    @ApiOperation(value = "redisGet")
    public ApiResult<List> redisGet(@PathVariable("url") String url) {
        List<Object> objects = redisUtil.lGet(HTTP_REDIS_PREFIX + url);
        return ApiResult.valueOf(objects);
    }

    @PostMapping("/{publishNo}")
    @ApiOperation(value = "根据token查看已部署仪表盘")
    public ApiResult<DashboardVO> queryByPublishNo(@DashboardAuth @PathVariable("publishNo") String publishNo) throws IOException {
        if (StringUtils.isEmpty(publishNo)) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        Map<String, String> queryParam = new ConcurrentHashMap<>();
        queryParam.put("publishNo", String.valueOf(publishNo));
        Response response = knowledgeGraphService.queryNebula(queryParam, "publishNo");
        DashboardDTO dashboard = dashboardService.queryByPublishNo(publishNo);
        if ((response == null || response.code() == 502) && dashboard == null) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR, null, "查无此仪表盘");
        }
        DashboardVO ret = DozerUtil.mapper(dashboard, DashboardVO.class);
        if (ret == null) {
            ret = new DashboardVO();
        }
        String str = response.body().string();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode responseNode = mapper.readTree(str);
        if (response != null && responseNode.get("code").asInt() == 200) {
            JsonNode dataList = responseNode.get("data");
            //ret.setLayout(assembleLayout(dashboard.getLayout()));
            ret.setPublishLayout(dataList.toString());
        } else {
            ret.setPipelineId(projectService.getDefaultPipelineId(ret.getProjectId()));
            ret.setLayout(assembleLayout(dashboard.getLayout()));
            String publishLayout = dashboard.getPublishLayout();
            if (StringUtils.isNotEmpty(publishLayout)) {
                ret.setPublishLayout(assemblePublishLayout(publishLayout));
            }
        }
        return ApiResult.valueOf(ret);
    }

    @PostMapping("/trigger/{publishNo}")
    @ApiOperation(value = "根据token查看已部署仪表盘是否需要执行")
    public ApiResult<Boolean> trigger(@PathVariable("publishNo") String publishNo) throws InterruptedException {
        if (StringUtils.isEmpty(publishNo)) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        DashboardDTO dashboard = dashboardService.queryByPublishNo(publishNo);
        return ApiResult.valueOf(dashboardService.triggerIfNeed(dashboard));
    }

    @PostMapping("/trigger2/{publishNo}")
    @ApiOperation(value = "根据token查看已部署仪表盘是否需要执行")
    public ApiResult<DashboardTriggerResultVO> trigger2(@PathVariable("publishNo") String publishNo) throws InterruptedException {
        if (StringUtils.isEmpty(publishNo)) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        DashboardDTO dashboard = dashboardService.queryByPublishNo(publishNo);
        return ApiResult.valueOf(dashboardService.triggerIfNeed2(dashboard));
    }


    /**
     * 可视化 构建允许多个仪表盘
     *
     * @param vo
     * @return
     */
    @ApiGroup(groups = DashboardVO.ProjectId.class, requestExecution = ApiExecutionEnum.INCLUDE)
    @PostMapping("/queryByProjectId")
    @ApiOperation(value = "根据项目id获取仪表盘列表")
    public ApiResult<List<DashboardVO>> queryByProjectId2(@ProjectAuth(auth = ProjectAuthEnum.READ) @RequestBody DashboardVO vo) {
        List<DashboardDTO> dashboards = dashboardService.queryByProjectId2(vo.getProjectId());
        if (dashboards == null) {
            return ApiResult.valueOf(ApiResultCode.SUCCESS);
        }
        List<DashboardVO> result = dashboards.stream().map(dashboard -> {
            DashboardVO dashboardVO = DozerUtil.mapper(dashboard, DashboardVO.class);

            dashboardVO.setLayout(assembleLayout(dashboard.getLayout()));

            dashboardVO.setPipelineId(projectService.getDefaultPipelineId(vo.getProjectId()));

            dashboardVO.setPublishTime(dashboard.getStatus() == 0 ? null : dashboard.getPublishTime());

            return dashboardVO;
        }).collect(Collectors.toList());
        return ApiResult.valueOf(result);
    }

    @PostMapping("/queryById")
    @ApiOperation(value = "根据id获取仪表盘")
    public ApiResult<DashboardVO> queryById(HttpServletRequest r, @RequestBody DashboardVO vo) {
        if (vo == null || vo.getId() == null) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        DashboardDTO dashboard = dashboardService.queryById(vo.getId());
        if (null != dashboard) {
            userProjectService.checkAuth(dashboard.getProjectId(), ProjectAuthEnum.READ.getValue());
        }
        DashboardVO ret = DozerUtil.mapper(dashboard, DashboardVO.class);
        ret.setLayout(assembleLayout(dashboard.getLayout()));
        return ApiResult.valueOf(ret);
    }

    @Deprecated
    @PostMapping(value = "/save")
    @ResponseBody
    @ApiOperation(value = "新增仪表盘", notes = "新增仪表盘")
    public ApiResult<DashboardVO> save(HttpServletRequest r, @RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER) DashboardVO vo) {
        if (vo == null || vo.getProjectId() == null) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        DashboardDTO dashboard = vo.toDashboard();
        dashboardService.save(dashboard);
        vo = dashboard.view();
        vo.setLayout(assembleLayout(dashboard.getLayout()));
        projectService.updateGmtModify(vo.getProjectId());
        return ApiResult.valueOf(vo);
    }

    @PostMapping(value = "/copy")
    @ResponseBody
    @ApiOperation(value = "拷贝仪表盘", notes = "拷贝仪表盘")
    public ApiResult<Long> copy(HttpServletRequest r, @RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER) JSONObject params) {
        try {
            Long dashboardID = dashboardService.copy(params.getLong("dashboardId"));
            projectService.updateGmtModify(params.getLong("projectId"));
            return ApiResult.valueOf(dashboardID);
        } catch (Exception e) {
            logger.error("API /dashboard/copy failed, since {}", e.getMessage());
            return ApiResult.error(ApiResultCode.SYS_ERROR, e.getMessage());
        }
    }

    @PostMapping(value = "/upload/bg")
    @ResponseBody
    @ApiOperation(value = "上传画布背景图片 (16MB)", notes = "上传画布背景图片")
    public ApiResult<Long> uploadBackgroundImg(@RequestParam(value = "picture", required = true) MultipartFile picture, Long dashboardId) {
        DashboardDTO dashboardDTO = dashboardService.queryById(dashboardId);
        if (ObjectUtil.isNull(dashboardDTO)) {
            return ApiResult.valueOf(ApiResultCode.DATA_NULL, null, "该画布不存在，请检查入参dashboardId");
        }
        try {
            if (ObjectUtil.isNotNull(picture)) {
                InputStream in = picture.getInputStream();
                String fileName = picture.getOriginalFilename();
                String lowerCaseName = fileName != null ? fileName.toLowerCase() : "";
                if (!lowerCaseName.endsWith(".jpg") && !lowerCaseName.endsWith(".png")
                        && !lowerCaseName.endsWith(".bmp") && !lowerCaseName.endsWith(".jpeg")) {
                    return ApiResult.valueOf(ApiResultCode.IMAGE_FORMAT_ERROR);
                }
                if (picture.getSize() == 0) {
                    return ApiResult.valueOf(ApiResultCode.CONTENT_ERROR);
                }
                dashboardDTO.setBackgroundImg(Constant.PIPELINE_SNAPSHOT_PICTURE_PREFIX + FileUtil.inputStream2Base64(in));
            } else {
                dashboardDTO.setBackgroundImg(null);
            }
        } catch (Exception e) {
            logger.error("upload background image for dashboard failed, since {}", e.getMessage());
        }
        dashboardService.update(dashboardDTO);
        return ApiResult.valueOf(ApiResultCode.SUCCESS);
    }


    @PostMapping(value = "/update")
    @ResponseBody
    @ApiOperation(value = "更新仪表盘", notes = "更新仪表盘")
    public ApiResult<DashboardVO> update(
            HttpServletRequest r,
            @RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER) @Valid DashboardVO vo
    ) throws ExecutionException {
        if (vo == null || vo.getId() == null || vo.getProjectId() == null) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }
        DashboardDTO dashboard = vo.toDashboard();
        if (dashboardManagerService.notSnapshotBefore(r, dashboard))
            dashboardManagerService.update(r, dashboardService.queryById(vo.getId()));
        dashboardService.update(dashboard);
        if (!dashboardManagerService.sameAsSnapshot(r, dashboard)) {
            dashboardManagerService.update(r, dashboardService.queryById(vo.getId()));
        }
        DashboardVO ret = DozerUtil.mapper(dashboard, DashboardVO.class);
        ret.setLayout(assembleLayout(dashboard.getLayout()));
        projectService.updateGmtModify(vo.getProjectId());
        return ApiResult.valueOf(ret);
    }

    @PostMapping(value = "/snapshot")
    @Transactional
    @ApiOperation(value = "回复仪表盘", notes = "恢复仪表盘")
    public ApiResult<String> snapshot(
            HttpServletRequest r,
            @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER, checkLock = true)
            @RequestBody @PipelineAuth() JSONObject params) {
        try {
            if (params.get("snapshot").equals("redo")) {
                dashboardManagerService.redo(r, params.getLong("dashboardId"));
            } else {
                dashboardManagerService.undo(r, params.getLong("dashboardId"));
            }
            return ApiResult.valueOf("成功");
        } catch (SnapshotManager.SnapshotException | ExecutionException e) {
            String tip = String.format(
                    params.get("snapshot").equals("redo") ? "重做失败: %s" : "撤销失败: %s",
                    e.getMessage());
            logger.warn(tip, e);
            return ApiResult.valueOf(ApiResultCode.REDO_AND_UNDO_ERROR, null, tip);
        }
    }

    @ApiGroup(groups = DashboardVO.IdCombo.class, requestExecution = ApiExecutionEnum.INCLUDE)
    @PostMapping(value = "/deleteById")
    @ResponseBody
    @Transactional
    @ApiOperation(value = "删除仪表盘", notes = "删除仪表盘")
    public ApiResult<Boolean> deleteById(@RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER) DashboardVO vo) {
        if (vo == null || vo.getId() == null) {
            return ApiResult.valueOf(ApiResultCode.PARAM_ERROR);
        }

        DashboardDTO exist = dashboardService.queryById(vo.getId());
        if (exist.getStatus() == 1) {
            return ApiResult.valueOf(ApiResultCode.SYS_ERROR, null, "该看板已发布不允许删除!");
        }
        String layout = exist.getLayout();
        if (StringUtils.isNotEmpty(layout)) {
            List<Long> deleteWidgetIds = Lists.newArrayList();
            JSONObject layoutJson = JSONObject.parseObject(layout);
            JSONArray gridItems = layoutJson.getJSONArray("gridItems");
            for (int i = 0; i < gridItems.size(); i++) {
                JSONObject widgetJson = gridItems.getJSONObject(i);
                Long widgetId = widgetJson.getLong("widgetId");
                deleteWidgetIds.add(widgetId);
            }
            widgetService.deleteByIds(null, deleteWidgetIds);
        }
        dashboardService.delete(vo.getId());
        //逻辑删除
        //DashboardDTO dashboardDTO = DozerUtil.mapper(vo, DashboardDTO.class);

        // dashboardService.update(dashboardDTO);
        projectService.updateGmtModify(vo.getProjectId());
        return ApiResult.valueOf(ApiResultCode.SUCCESS, true);
    }

    @ApiGroup(groups = DashboardVO.UnStuck.class, requestExecution = ApiExecutionEnum.INCLUDE)
    @PostMapping(value = "/unstuck")
    @Transactional
    @ResponseBody
    @ApiOperation(value = "下架仪表盘", notes = "下架仪表盘")
    public ApiResult<Object> unstuck(@RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.DEVELOPER) DashboardVO vo) {
        try {
            dashboardService.unstuck(vo);
            return ApiResult.valueOf(true);
        } catch (DataScienceException e) {
            return ApiResult.error(ApiResultCode.DASHBOARD_UNSTUCK_FAILED, e.getMessage());
        }
    }

    @ApiGroup(groups = DashboardVO.Download.class, requestExecution = ApiExecutionEnum.INCLUDE)
    @PostMapping(value = "/download")
    @Transactional
    @ApiOperation(value = "下载报告", notes = "下载报告")
    public void download(HttpServletRequest request, HttpServletResponse response, @RequestBody @Valid DashboardVO vo) {
        DashboardDTO dashboardDTO = dashboardService.queryById(vo.getId());
        if (DashboardTypeEnum.isReport(dashboardDTO.getType())) {  //只有报告类型的画板 才允许下载
            vo.setTemplateType("test1");//使用test1模板 名称与 common/resources/template/word里面的文件名相同 TODO
            //

            File generatedFile = dashboardService.generateFile(vo.getTemplateType(), dashboardDTO.getLayout(), vo.getImgUrlMap());
            InputStream fin = null;
            OutputStream out = null;
            try {
                fin = new FileInputStream(generatedFile);
                response.setCharacterEncoding(WordTemplateConstant.UTF_8);
                response.setContentType(WordTemplateConstant.CONTENT_TYPE_WORD);
                String agent = request.getHeader(WordTemplateConstant.USER_AGENT);
                String filenameEncoder = URLEncoder.encode(generatedFile.getName(), WordTemplateConstant.UTF_8);
                // 根据不同的浏览器进行不同的判断
                if (agent.contains(WordTemplateConstant.MSIE)) {
                    // IE浏览器
                    filenameEncoder = filenameEncoder.replace("+", " ");
                } else if (agent.contains(WordTemplateConstant.FIREFOX)) {
                    // 火狐浏览器
                    filenameEncoder = "=?utf-8?B?" + Base64.getEncoder().encode(generatedFile.getName().getBytes(WordTemplateConstant.UTF_8)) + "?=";
                } else {
                    // 其它浏览器
                    filenameEncoder = URLEncoder.encode(generatedFile.getName(), WordTemplateConstant.UTF_8);
                }
                response.setHeader(WordTemplateConstant.ACCESS_CONTROL_ALLOW_ORIGIN, "*");//所有域都可以跨
                response.setHeader(WordTemplateConstant.CONTENT_TYPE, WordTemplateConstant.CONTENT_TYPE_STEAM);//二进制  流文件
                response.setHeader(WordTemplateConstant.CONTENT_DISPOSITION, "attachment;filename=" + filenameEncoder + ".doc");//下载及其文件名
                response.setHeader(WordTemplateConstant.CONNECTION, WordTemplateConstant.CLOSE);//关闭请求头连接
                response.setContentType(WordTemplateConstant.CONTENT_TYPE_DOWNLOAD); //设置文件在浏览器打开还是下载
                out = response.getOutputStream();
                byte[] buffer = new byte[WordTemplateConstant.BYTE_512];
                int bytesToRead = WordTemplateConstant.NUM_MINUS_1;
                // 通过循环将读入的Word文件的内容输出到浏览器中
                while ((bytesToRead = fin.read(buffer)) != -1) {
                    out.write(buffer, 0, bytesToRead);
                }

            } catch (Exception e) {
                logger.error("download word file failed, since {}", e.getMessage());
                throw new DataScienceException(BaseErrorCode.WORD_GENERATED_FAILED, e);
            } finally {
                try {
                    if (fin != null) {
                        fin.close();
                    }
                    if (out != null) {
                        out.close();
                    }
                    if (generatedFile != null) {
                        generatedFile.delete();
                    }
                } catch (IOException e) {
                    logger.error("download word file failed, since {}", e.getMessage());
                    throw new DataScienceException(BaseErrorCode.WORD_GENERATED_FAILED, e);
                }
            }
        }
    }

    @PostMapping(value = "/publish")
    @Transactional
    @ResponseBody
    @ApiOperation(value = "部署仪表盘", notes = "部署仪表盘")
    public ApiResult<String> publish(@RequestBody @ProjectRoleAuth(role = ProjectRoleAuthEnum.ADMIN) @Valid DashboardVO vo) {
        if (!dashboardService.checkWidgetsBeforePublish(vo.getId())) {
            return ApiResult.valueOf(ApiResultCode.DASHBOARD_PUBLISH_FAILED, null, "图表不存在，请配置完成后再发布");
        }
        vo.setIsComplex(true);//每次都复制
        if (ObjectUtil.isNull(vo.getVisitorIds())) {
            //如果不填，默认只有自己可以访问
            vo.setVisitorIds(String.valueOf(JwtUtil.getCurrentUserId()));
        }
        String publishNo = dashboardService.publish(vo);
        return ApiResult.valueOf(publishNo);
    }

    private String assembleLayout(String layout) {
        return assembleLayout(layout, true);
    }

    private String assembleLayout(String layout, Boolean needRemove) {
        JSONObject layoutJson = new JSONObject();
        if (StringUtils.isNoneEmpty(layout)) {
            layoutJson = JSONObject.parseObject(layout);
            JSONArray widgets = layoutJson.getJSONArray(GRID_ITEMS);
            if (CollectionUtil.isNotEmpty(widgets)) {
                Iterator<Object> iterator = widgets.iterator();
                while (iterator.hasNext()) {
                    JSONObject widgetJson = (JSONObject) iterator.next();
                    //下面查不到widget就从dashboard展示中删除
                    Long widgetId = widgetJson.getLong(WIDGET_ID);
                    WidgetDTO widgetDTO = widgetService.queryById(widgetId);
                    if (null == widgetId || null == widgetDTO) {
                        if (needRemove) iterator.remove();
                        continue;
                    }
                    if (!widgetDTO.getType().equalsIgnoreCase(WidgetTypeEnum.TEXT.getDesc())) {
                        widgetJson.put("config", widgetDTO.getDataJson());
                    }
                }
                layoutJson.put("gridItems", widgets);
            }
        }
        return layoutJson.toJSONString();
    }

    private String assemblePublishLayout(String publishLayout) {
        JSONObject layoutJson = new JSONObject();
        if (StringUtils.isNoneEmpty(publishLayout)) {
            layoutJson = JSONObject.parseObject(publishLayout);
            JSONArray widgets = layoutJson.getJSONArray(GRID_ITEMS);
            if (CollectionUtil.isNotEmpty(widgets)) {
                Iterator<Object> iterator = widgets.iterator();
                while (iterator.hasNext()) {
                    JSONObject widgetJson = (JSONObject) iterator.next();
                    //下面查不到widget就从dashboard展示中删除
                    Long widgetId = widgetJson.getLong(WIDGET_ID);
                    WidgetDTO widgetDTO = widgetService.queryById(widgetId);
                    if (null == widgetId || null == widgetDTO) {
                        continue;
                    }
                    if (!widgetDTO.getType().equalsIgnoreCase(WidgetTypeEnum.TEXT.getDesc())) {
                        widgetJson.put("config", widgetDTO.getPublishDataJson());
                    }
                }
                layoutJson.put("gridItems", widgets);
            }
        }
        return layoutJson.toJSONString();
    }
}
